import uuidv4 from 'uuid/v4'
import { EventEmitter } from '../base/events'
import { reactive } from 'vue'
import { conn } from '@beshell/conn-ts/Connection'
import { Protocol } from '@beshell/conn-ts/Protocol'
import {PartBase} from './parts/PartBase'
import { Stud } from './Stud'
import { Wire } from './conn/Wire'
import { Bus } from './conn/Bus'
import {PartHost,PartSlaver} from './parts/PartBase'
import {BeCore16, BeCore32, BePad, BeVision} from './parts/PartESP32'
import {BeColorDetector} from './parts/BeColorDetector'
import {BeJoystick} from './parts/BeJoystick'
import {BePoser} from './parts/BePoser'
import {BeLED, BeLEDRGB} from './parts/BeLED'
import {BeSwitch} from './parts/BeSwitch'
import {BeScreen096i} from './parts/BeScreen096i'
import {BeScreen144i} from './parts/BeScreen144i'
import {BeServo, BeMotor} from './parts/Servo'
import {BePower200} from './parts/BePower'
// import { Canvas } from '../graph/Canvas'
import {UIModel} from "./UIModel"
import JSZip from 'jszip'

export const DocVer = [1, 1, 0]

export const mapPartNameToClass: any= {
    register(clazz: any, name?:string) {
        this[name||clazz.name] = clazz
    }
}
mapPartNameToClass.register(BeCore16)
mapPartNameToClass.register(BeCore32)
mapPartNameToClass.register(BePad)
mapPartNameToClass.register(BeVision)
mapPartNameToClass.register(BeLED)
mapPartNameToClass.register(BeLEDRGB)
mapPartNameToClass.register(BeMotor)
mapPartNameToClass.register(BeServo)
mapPartNameToClass.register(BePoser)
mapPartNameToClass.register(BeColorDetector)
mapPartNameToClass.register(BeJoystick)
mapPartNameToClass.register(BeSwitch)
mapPartNameToClass.register(BeScreen096i)
mapPartNameToClass.register(BeScreen144i)
mapPartNameToClass.register(BePower200)

export const mapHostPartIDToClass:any = {
    1: BeCore16 ,
    2: BeCore32 ,
    3: BePad ,
    19: BeVision ,
}


export interface AppPkgMeta {
    name: string,
    version: string ,
    docver: string ,
    uuid: string ,
    title: string ,
    desc: string ,
    createtime: number ,
    modifytime: number ,
    parts: number ,
}


export class AppModel extends EventEmitter {

    public lstParts: PartBase[] = []

    public isEmpty: boolean = true

    public masterPart: PartHost | undefined

    public lstWires: Wire[] = []
    public lstBus: Bus[] = []

    public bMockup = false

    // package.json
    public pkgmeta: AppPkgMeta = reactive({
        name: "become-app" ,
        version: "1.0.0" ,
        uuid: uuidv4() ,
        title: "这个模型还没有名字" ,
        desc: "" ,
        createtime: 0 ,
        modifytime: 0 ,
        parts: 0 ,
        docver: DocVer.join(".")
    })

    public json: any = {
        parts: [] ,
        wires: [] ,
    }
    public uiJson : any = {}
    public blocklyXML: string = ''

    public static active: AppModel

    // public graphCanvas: Canvas
    public ui = new UIModel

    public folderPath = ""


    constructor() {
        super()
        this.pkgmeta.createtime = Date.now()
        this.pkgmeta.modifytime = Date.now()
    }

    public createPart(name: string): PartBase {
        if(!mapPartNameToClass[name]) {
            throw new Error("unknow class: "+name)
        }
        return new mapPartNameToClass[name]( this )
    }

    public addPart(part: string|PartBase): PartBase {

        if( typeof(part)=="string" ){
            part = this.createPart(part)
        }

        this.lstParts.push(part)

        if(!this.masterPart && part instanceof PartHost) {
            this.masterPart = part
        }

        this.emit("part.add", part)

        return part
    }
    
    public removePart(part: PartBase) {
        this.lstParts.removeFirst(part)
        this.emit("part.remove", part)
        part.destroy()
    }

    public async eachPart(callback: (part: PartBase, idx: number)=>void, clazz?: new(...arg:any[])=>PartBase) {
        let ret: any = undefined
        for(let i = 0; i<this.lstParts.length; i++) {
            if(!clazz || this.lstParts[i] instanceof clazz) {
                ret = callback(this.lstParts[i], i)
                if(ret instanceof Promise)
                    ret = await ret
                if( ret!=undefined )
                    return ret
            }
        }
    }
    public reducePart(callback: (prev: any, part: PartBase, idx: number, array: PartBase[])=>void, clazz?: new(...arg:any[])=>PartBase, initVal?: any[]) {
        return this.lstParts.reduce((prev: any, part: PartBase, idx: number, array: PartBase[])=>{
            if(!clazz || part instanceof clazz) {
                return callback(prev, part, idx, array)
            }
            else {
                return prev
            }
        }, initVal)
    }

    public findPartFromInsIdx(clazz: new(...arg:any[])=>PartBase, insIdx: number): PartBase|null {
        for(let part of this.lstParts) {
            if(part.instanceIndex==insIdx && part instanceof clazz) {
                return part
            }
        }
        return null
    }
    public findPartFromUUID(uuid: string): PartSlaver|null {
        for(let part of this.lstParts) {
            if(part.uuid==uuid) {
                return part as PartSlaver
            }
        }
        return null
    }

    // // 
    public createWire(from: Stud, to: Stud): Wire {

        // let isPartReady = (stud: Stud) => {
        //     return (stud.part instanceof PartSlaver)? stud.part.isConnectionReady(): false
        // }

        // let fromPartReady = isPartReady(from)
        // let toPartReady = isPartReady(to)

        var newWire = new Wire(from, to)
        this.lstWires.push(newWire)

        from.wires.push(newWire)
        to.wires.push(newWire)

        var bus:Bus|undefined = undefined
        
        // 合并两根bus
        if( from.bus && to.bus && to.bus!=from.bus ) {
            this.lstBus.removeFirst(to.bus)
            bus = from.bus
            bus.merge(to.bus)
        } 
        // 合并到已有的bus上
        else if(bus=(from.bus||to.bus)) {
            // nothing todo ...
        }
        // 新建bus
        else {
            bus = new Bus()
            this.lstBus.push(bus)
        }
        
        bus.addWire(newWire)
        
        // console.log("wire.connect", newWire)
        this.emit("wire.connect", newWire)

        return newWire
    }

    public disconnectWire(wire: Wire) {

        if(wire.bus) {
            var oriBus = wire.bus
            var newBus = wire.bus.removeWire(wire)
            // 分离出了新的bus
            if(newBus) {
                this.lstBus.push(newBus)
            }
            // wire 断开后，所在的 bus 已空
            if( !oriBus.wires.length ) {
                this.lstBus.removeFirst(oriBus)
            }
        }

        this.lstWires.removeFirst(wire)

        this.emit("wire.disconnect", wire)

        wire.destroy()
    }

    public eachWire(callback: (wire: Wire, idx: number)=>void): any {
        for(let i = 0; i<this.lstWires.length; i++) {
            callback(this.lstWires[i], i)
        }
    }

    public serialize(): any {
        let doc: any = {}

        // 零件
        if(this.lstParts.length) {
            doc.parts = []
            for(let part of this.lstParts) {
                doc.parts.push( part.serialize() )
            }
        }

        // 连线
        if(this.lstWires.length) {
            doc.wires = []
            for(let wire of this.lstWires) {
                doc.wires.push(wire.serialize())
            }
        }

        return doc
    }

    public unserialize(json: any) {

        // 加载零件
        if(json.parts) {
            for(let jsonPart of json.parts) {
                let part = this.createPart(jsonPart.clazz)
                part.unserialize(jsonPart)
                this.addPart(part)
            }
        }

        // 加载连线
        if(json.wires) {
            for(let jsonWire of json.wires) {
                let fromPart = this.findPartFromUUID(jsonWire.from.part)
                let toPart = this.findPartFromUUID(jsonWire.to.part)
                if(fromPart && toPart) {
                    let from = fromPart.findStudFromName(jsonWire.from.stud)
                    let to = toPart.findStudFromName(jsonWire.to.stud)
                    if(from && to){
                        this.createWire(from, to)
                                .uuid = jsonWire.uuid
                    }
                }
            }
        }
    }

    public async remoteSetup() {
        return this.eachPart(async (part)=>{
            if(part instanceof PartSlaver) {
                await part.remoteSetup()
            }
        })
    }

    static trimTitle(title:string, max:number) {
        let titleBytes = Protocol.toUTF8Array(title)
        for(let i=titleBytes.length-1; i>=0; i--) {
            if(titleBytes.length<=max)
                return Protocol.fromUTF8Array(titleBytes)
            
            if(titleBytes[i]>127) {
                titleBytes.length = titleBytes.length - 3
                i-= 2
            }
            else {
                titleBytes.length = titleBytes.length - 1
            }
        }
    }

    public archive(extra: any=null, type: JSZip.OutputType='uint8array') {
        
        let zip = new JSZip()
        
        this.json = this.serialize()
        if(extra) {
            for(let key in extra) {
                this.json[key] = extra[key]
            }
        }

        zip.file("model.json", JSON.stringify(this.json))
        zip.file("ui.json", JSON.stringify(this.ui.serialize()))
        zip.file("blockly.xml", "<xml />")

        return zip.generateAsync({type})
    }
    public async unarchive(data: Uint8Array) {
        // console.log(data.length)
        let zip = new JSZip()
        await zip.loadAsync(data)
        
        try{
            let str = await zip.file("model.json")?.async("string")
            this.json = JSON.parse(str as any)
            this.unserialize(this.json)
        } catch(e) {
            console.log(e)
        }
        
        try{
            let str = await zip.file("ui.json")?.async("string")
            this.uiJson = JSON.parse(str as any)
        } catch(e) {
            console.log(e)
        }
    }

    public async save(extra?: any, echo:any=null) {
        if( !await conn.console.call(`beapi.fs.existsSync("${this.folderPath}")`) ) {
            throw new Error("Remote model folder not exists:"+this.folderPath) 
        }

        let data = await this.archive(extra) as Uint8Array
        await conn.active?.writeFile(this.folderPath+'/workspace.zip', data, (progress:number)=>{
            echo && echo(`正在写入文件: ${this.folderPath}/workspace.zip [${Math.round(progress*100)}%]`)
        })

        // package.json
        await this.savePackageMeta(echo)
    }

    public async savePackageMeta(echo:any=null) {
        if(!conn.isConnected)
            return false
        
        this.pkgmeta.modifytime = Date.now()
        this.pkgmeta.parts = this.lstParts.length
        let strjson = JSON.stringify(this.pkgmeta, null, 2)
        
        echo && echo(`正在写入文件: ${this.folderPath}/package.json`)
        return await conn.active?.writeFile(this.folderPath+'/package.json', strjson)
    }

    public async loadPackageMeta(path:string ) {
        if(!conn.isConnected) {
            return false
        }
        let pkgmeta: any = await conn.active?.readFile(path, true, true)
        if(!pkgmeta) {
            throw new Error("Not a BECOME package")
        }
        this.pkgmeta = pkgmeta
    }

    public async load(remoteFolder: string, echo:any=null) {
        if(!conn.isConnected){
            return false
        }

        this.folderPath = remoteFolder

        // package.json
        echo && echo(`正在载入文件: ${remoteFolder}/package.json`)
        await this.loadPackageMeta(remoteFolder+'/package.json') ;

        // model.json, ui.json, blockly.xml from workspace.zip
        if(await conn.active?.call(`beapi.fs.existsSync("${remoteFolder}/workspace.zip")`)) {
            let data = await conn.active?.readFile(remoteFolder+'/workspace.zip', false, false)
            if(data) {
                this.unarchive(data)
            }
        }
        // new app model
        else {
            // 创建主机零件
            this.createHostPart() ;
        }
        

        if(!this.json) {
            throw new Error("Failed to load model.json")
        }
        this.unserialize(this.json)

        return true
    }

    public generateSetupCode(indent=0) {
        let code = ''

        // setup
        for(let part of this.lstParts){
            let line = part.generateSetupCode(indent)
            if(line) {
                code+= line + "\r\n\r\n"
            }
        }
        return code
    }
    
    public generateBeginCode(indent=0) {
        let code = ''

        // setup
        for(let part of this.lstParts){
            let line = part.generateBeginCode(indent)
            if(line) {
                code+= line + "\r\n\r\n"
            }
        }
        return code
    }

    private instanceNameExists(altName: string) {
        for(let part of this.lstParts) {
            if( part.instanceName == altName ){
                return true
            }
        }
        return false
    }

    public makePartInstanceName(part: PartBase) {
        let nameTmp: string = part.json.instanceName || part.json.title || part.constructor.name
        nameTmp = nameTmp.replace(/\s/gm, "")

        let altName = ''
        for(let idx=1; ;idx++) {
            altName = nameTmp.replace("${idx}", idx.toString())
            if(altName == nameTmp){
                altName+= idx
            }
            if(!this.instanceNameExists(altName)) {
                break
            }
        }

        return altName
    }
}